package com.blustreaksw.cyclecoach.btle;

import java.util.Calendar;
import java.util.List;
import java.util.UUID;

import org.json.JSONException;
import org.json.JSONObject;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.util.Log;

/***********************************************************************
 * Encapsulates functionality required to communicate with a Bluetooth
 * LE (i.e. Bluetooth Smart) device. On creation requires a device
 * address, device type, application context, and caller definition of
 * BtleDeviceCallback interface.
 * 
 * @author Tomas Rivera
 *
 ***********************************************************************/
public class BtleDevice {
	
	private final boolean DEBUG_ENABLED = true;
	private void logDebug(String s) { if(DEBUG_ENABLED) Log.d(TAG, s); }
	private boolean mDeviceConnected = false;
	
	/***********************************************************************
     * Static class definitions
     ***********************************************************************/
	private static final int DEFAULT_REFERENCE_ID = -1;
	private final static String TAG = "BtleDevice";
	
	/***********************************************************************
     * Interface that allows the BtleDevice to transfer data that was
     * gathered from the sensor(s) on the physical device, and inform
     * the client of any connectivity changes.
     ***********************************************************************/
	public interface BtleDeviceCallback {
		/********************************************************************************
		 * Transfer data gathered from a cycling power meter. Caller handles translation of the
		 * characteristic data packet into its component member variables.
		 * @param 	timestamp
		 * 	Unix epoch time stamp when data received from device
		 * @param	referenceId
		 * 	Client defined numeric tag
		 * @param	bpm
		 * 	Instantaneous beats per minute
		 * @see		<a href="https://developer.bluetooth.org/gatt/profiles/Pages/ProfileViewer.aspx?u=org.bluetooth.profile.heart_rate.xml">
		 * 			org.bluetooth.profile.heart_rate</a>
		 ********************************************************************************/
		void transferCharacteristicData_heartRate(BtleDevice device,
				long timestamp, int referenceId, int bpm);
		
		/********************************************************************************
		 * Transfer data gathered from a cycling power meter. Caller handles translation of the
		 * characteristic data packet into its component member variables.
		 * @param	timestamp
		 * 	Unix epoch time stamp when data received from device
		 * @param	referenceId
		 * 	Client defined numeric tag
		 * @param	instantPower
		 * 	Instantaneous power reading in watts
		 * @param	pedalPowerBalance
		 * 	Instantaneous pedal power balance reading in percentage with a resolution of 1/2.
		 * @param	accumulatedTorque
		 *  Accumulated torque in newton meters with a resolution of 1/32.
		 * @param	cumWheelRevs
		 * 	Cumulative wheel revolutions reported by sensor. This is recorded as an unsigned
		 *  32-bit value by the device.
		 * @param 	cumCrankRevs
		 * 	Cumulative crank revolutions reported by sensor. This is recorded as an unsigned
		 * 	16-bit integer by the device.
		 * @param	lastWheelTime
		 * 	Time since wheel rotation sensor last posted a value, in 1/1024 second increments.
		 *  This is recorded as an unsigned 16-bit integer by the device.
		 * @param	lastCrankTime
		 * 	Time since crank rotation sensor last posted a value, in 1/1024 second increments.
		 * 	This is recorded as an unsigned 16-bit integer by the device.
		 * @param	maxForce
		 * 	Maximum force magnitude, unit is in newtons with a resolution of 1.
		 * @param	minForce
		 *  Minimum force magnitude, unit is in newtons with a resolution of 1.
		 * @param	maxTorque
		 * 	Maximum torque magnitude, unit is in newton meters with a resolution of 1/32.
		 * @param	minTorque
		 *  Minimum torque magnitude, unit is in newton meters with a resolution of 1/32.
		 * @param	maxAngle
		 * 	Maximum angle, unit is in degrees with a resolution of 1.
		 * @param	minAngle
		 *  Minimum angle, unit is in degrees with a resolution of 1.
		 * @param	topDeadSpotAngle
		 *  Top dead spot angle, unit is in degrees with a resolution of 1.
		 * @param	bottomDeadSpotAngle
		 * 	Bottom dead spot angle, unit is in degrees with a resolution of 1.
		 * @param	accumulatedEnergy
		 *  Accumulated energy, Unit is in kilojoules with a resolution of 1.
		 * 							
		 * @see
		 * 	<a href="https://developer.bluetooth.org/gatt/profiles/Pages/ProfileViewer.aspx?u=org.bluetooth.profile.cycling_power.xml">
		 * 	org.bluetooth.profile.cycling_power</a>
		 ********************************************************************************/
		void transferCharacteristicData_cyclePower(BtleDevice device,
				long timestamp, int referenceId, int instantPower, double pedalPowerBalance, double accumulatedTorque,
				int cumWheelRevs, int cumCrankRevs, int lastWheelTime, int lastCrankTime,
				int maxForce, int minForce, double maxTorque, double minTorque,
				int maxAngle, int minAngle, int topDeadSpotAngle, int bottomDeadSpotAngle, int accumulatedEnergy);
		
		/********************************************************************************
		 * Transfer data gathered from a cycling speed and cadence sensor. Caller handles  
		 * translation of the characteristic data packet into its component member variables.
		 * @param	timestamp
		 * 	Unix epoch time stamp when data received from device
		 * @param	referenceId
		 * 	Client defined numeric tag
		 * @param	cumWheelRevs
		 * 	Cumulative wheel revolutions reported by sensor. This is recorded as an unsigned
		 *  32-bit value by the device.
		 * @param 	cumCrankRevs
		 * 	Cumulative crank revolutions reported by sensor. This is recorded as an unsigned
		 * 	16-bit integer by the device.
		 * @param	lastWheelTime
		 * 	Time since wheel rotation sensor last posted a value, in 1/1024 second increments.
		 *  This is recorded as an unsigned 16-bit integer by the device.
		 * @param	lastCrankTime
		 * 	Time since crank rotation sensor last posted a value, in 1/1024 second increments.
		 * 	This is recorded as an unsigned 16-bit integer by the device.
		 * @see
		 * 	<a href="https://developer.bluetooth.org/gatt/profiles/Pages/ProfileViewer.aspx?u=org.bluetooth.profile.cycling_speed_and_cadence.xml">
		 * 	org.bluetooth.profile.cycling_speed_and_cadence</a>
		 ********************************************************************************/
		void transferCharacteristicData_cycleSpeedCadence(BtleDevice device,
				long timestamp, int referenceId, int cumWheelRevs, int cumCrankRevs, int lastWheelTime, int lastCrankTime);
		
		/********************************************************************************
		 * Callback issued to the client that owns this object to inform it that the 
		 * device is now connected and is sending data.
		 * @param	btleDevice
		 * 	This BtleDevice object.
		 ********************************************************************************/
		void onSuccessfulDeviceConnection(BtleDevice btleDevice);
		
		/********************************************************************************
		 * Callback issued to the client when we timeout while trying to connect to, or 
		 * experience a specific error condition while trying to connect to the device.
		 * @param	address
		 * 	This BtleDevice object.
		 * @param	error
		 * 	Enum BtleDeviceError object that contains the error code and a string
		 *  description of the error.
		 ********************************************************************************/
		void onFailedDeviceConnection(BtleDevice btleDevice, BtleDeviceStatus error);
		
		/********************************************************************************
		 * Callback issued to the client when connection to device has been dropped.
		 * @param	address
		 * 	This BtleDevice object.
		 * @param	error
		 * 	Enum BtleDeviceError object that contains the error code and a string
		 *  description of the error.
		 ********************************************************************************/
		void onDeviceConnectionDropped(BtleDevice btleDevice, BtleDeviceStatus error);
	}
	
	
	/***********************************************************************
     * Definitions for converting BtleDevice settings to JSON.
     ***********************************************************************/
	private static final String JSON_BTLE_DEV_ADDRESS = "address";
	private static final String JSON_BTLE_DEV_TYPE = "type";
	private static final String JSON_BTLE_DEV_ACTIVE = "isActive";
	private static final String JSON_BTLE_DEV_REFERENCE_ID = "referenceId";
	
	/***********************************************************************
     * Converts BtleDevice member variables to JSON object to allow
     * for persistent storage of configuration.
     * 
     * @return	JSONObject containing device characteristics.
     ***********************************************************************/
	public JSONObject toJSON() throws JSONException {
		JSONObject json = new JSONObject();
		json.put(JSON_BTLE_DEV_ADDRESS, getBtleDeviceAddress());
		json.put(JSON_BTLE_DEV_ACTIVE, isSensorDataUploadEnabled());
		json.put(JSON_BTLE_DEV_REFERENCE_ID, getReferenceId());
		json.put(JSON_BTLE_DEV_TYPE, getBtleDeviceType().getValue());
		return json;
	}
	
	/***********************************************************************
     * Member variables used to handle connection and access to 
     * Bluetooth LE devices. 
     ***********************************************************************/
	private Handler mHandler = null;
	private BluetoothManager mBTManager = null;
	private BluetoothAdapter mBTAdapter = null;
	private BluetoothDevice  mBluetoothDevice = null;
	private BluetoothGatt    mBTGatt = null;
	private BluetoothGattService        mBTService = null;
	private BluetoothGattCharacteristic mBTValueCharacteristic = null;

	/***********************************************************************
     * Member variables and getters/setters for managing device address,
     * client reference identification, and Bluetooth LE device type.
     ***********************************************************************/
	private String mBtleDeviceName = null;
	private String mBtleDeviceAddress;
	private int mReferenceId;
	private BtleDeviceType mBtleDeviceType;
	private boolean mClientDataNotificationActive;
	private BtleDeviceCallback mDeviceCallback;
	private Context mAppContext;
	
	/********************************************************************************
	 * Key values used for thread synchronization.
	 ********************************************************************************/
	private Object mReferenceIdSyncKey = new Object();
	private Object mNotificationsActiveSyncKey = new Object();
	
	/********************************************************************************
	 * Retrieves device address stored in the object.
	 * 
	 * @return	HEX string device address
	 ********************************************************************************/
	public String getBtleDeviceAddress() {
		return mBtleDeviceAddress;
	}
	
	/********************************************************************************
	 * Sets the device address that the BtleDevice will target.
	 * 
	 * @param	btleDeviceAddress
	 * 	HEX string device address
	 ********************************************************************************/
	public void setBtleDeviceAddress(String btleDeviceAddress) {
		if(btleDeviceAddress == null) throw new NullPointerException();
		mBtleDeviceAddress = btleDeviceAddress;
	}
	
	
	/********************************************************************************
	 * Gets the client reference ID that is saved in the object.
	 * 
	 * @return int client reference ID
	 ********************************************************************************/
	public int getReferenceId() {
		synchronized(mReferenceIdSyncKey) {
			return mReferenceId;
		}
	}
	
	/********************************************************************************
	 * Sets the client reference ID to use on callbacks.
	 * 
	 * @param	referenceId
	 * 		int value to be passed back to the client when sending a
	 * 		characteristic data notification.
	 ********************************************************************************/
	public void setReferenceId(int referenceId) {
		synchronized(mReferenceIdSyncKey) {
			mReferenceId = referenceId;
		}
	}
	
	/********************************************************************************
	 * Gets the BtleDeviceType enum defined for the object.
	 * 
	 * @return enum that defines the Bluetooth LE device type.
	 ********************************************************************************/
	public BtleDeviceType getBtleDeviceType() {
		return mBtleDeviceType;
	}
	
	/********************************************************************************
	 * Sets the BtleDeviceType.
	 * 
	 * @param	btleDeviceType
	 * 		BtleDeviceType of the sensor the BtleDevice object is managing.
	 ********************************************************************************/
	public void setBtleDeviceType(BtleDeviceType btleDeviceType) {
		if(btleDeviceType == null) throw new NullPointerException();
		mBtleDeviceType = btleDeviceType;
	}
	
	/********************************************************************************
	 * Checks if the BtleDevice is currently set to send characteristic notifications
	 * to the client.
	 * 
	 * @return true, if notifications are enabled to the client.
	 ********************************************************************************/
	public boolean isSensorDataUploadEnabled() {
		synchronized(mNotificationsActiveSyncKey) {
			return mClientDataNotificationActive;
		}
	}
	
	/********************************************************************************
	 * Gets the manufacturer specified device name that was acquired from the
	 * physical Bluetooth LE device.
	 * 
	 * @return String manufacturer device name
	 ********************************************************************************/
	public String getDeviceName() {
		return mBtleDeviceName;
	}
	
	/*
	 * Helper function that allows Runnables and Callbacks to reference
	 * the BtleDevice object.
	 */
	private BtleDevice getBtleDevice() {
		return this;
	}
	
	/*
	 * Manages client data notification state.
	 */
	private void setNotificationEnabled() {
		synchronized(mNotificationsActiveSyncKey) {
			mClientDataNotificationActive = true;
		}
		logDebug("Notifications have been enabled for " + mBtleDeviceName);
	}
	private void setNotificationDisabled() {
		synchronized(mNotificationsActiveSyncKey) {
			mClientDataNotificationActive = false;
		}
		logDebug("Notifications have been disabled for " + mBtleDeviceName);
	}
	
	/***********************************************************************
     * Creates a new BtleDevice object.
     * 
     * @param	address
     * 	Device HEX address in string format
     * @param	type
     *  BtleDeviceType Enum device descriptor
     * @param	cb
     *  Callers interface definition for BtleDeviceCallback
     * @param	context
     *  Callers application context
     ***********************************************************************/
	public BtleDevice(
			String address, BtleDeviceType type, BtleDeviceCallback cb, Context context) {
		mHandler = new Handler();
		setNotificationDisabled();
		setBtleDeviceAddress(address);
		setBtleDeviceType(type);
		setReferenceId(DEFAULT_REFERENCE_ID);
		mDeviceCallback = cb;
		mAppContext = context;
		logDebug(String.format("Created BtleDevice, address: %s, type %s, ref: %d, active: %s",
				getBtleDeviceAddress(),
				getBtleDeviceType().toString(),
				getReferenceId(),
				isSensorDataUploadEnabled() ? "TRUE" : "FALSE"));
	}
	
	/***********************************************************************
     * Creates a new BtleDevice object using a JSONObject.
     * 
	 * @param	json
	 * 	JSONObject that contains device address, type and referenceId.
     * @param	cb
     *  Callers interface definition for BtleDeviceCallback
     * @param	context
     *  Callers application context
     ***********************************************************************/
	public BtleDevice(JSONObject json, BtleDeviceCallback cb, Context context) throws JSONException {
		mHandler = new Handler();
		if(json.getBoolean(JSON_BTLE_DEV_ACTIVE)) {
			setNotificationEnabled();
		} else {
			setNotificationDisabled();
		}
		
		setBtleDeviceAddress(json.getString(JSON_BTLE_DEV_ADDRESS));
		setBtleDeviceType(BtleDeviceType.fromInt(json.getInt(JSON_BTLE_DEV_TYPE)));
		setReferenceId(json.getInt(JSON_BTLE_DEV_REFERENCE_ID));
		mDeviceCallback = cb;
		mAppContext = context;
		logDebug(String.format("(JSON) Created BtleDevice, address: %s, type %s, ref: %d, active: %s",
				getBtleDeviceAddress(),
				getBtleDeviceType().toString(),
				getReferenceId(),
				isSensorDataUploadEnabled() ? "TRUE" : "FALSE"));	
	}
	
	/***********************************************************************
     * Attempts to connect to the BtleDevice defined by the constructor.
     * Return is handled by callback as defined by the client.
     * 
     * <br>On Success: onSuccessfulDeviceConnection() is called
     * <br>On Failure: onFailedDeviceConnection() is called with enum
     * BtleDeviceError and the device address.
     ***********************************************************************/
	public void initiateDeviceConnection() {
		
		if(!initBt()) {
			mDeviceCallback.onFailedDeviceConnection(this,
					BtleDeviceStatus.BTLE_ANDROID_BLUETOOTH_SERVICE_UNAVAILABLE);
			return;
		} else if(!isBleAvailable()) {
			mDeviceCallback.onFailedDeviceConnection(this,
					BtleDeviceStatus.BTLE_BLUETOOTH_LE_NOT_AVAILABLE);
			return;
		} else if(!isBtEnabled()) {
			mDeviceCallback.onFailedDeviceConnection(this,
					BtleDeviceStatus.BTLE_BLUETOOTH_RADIO_DISABLED);
			return;
		} else if(!BluetoothAdapter.checkBluetoothAddress(mBtleDeviceAddress)) {
			mDeviceCallback.onFailedDeviceConnection(this,
					BtleDeviceStatus.BTLE_ADDRESS_INVALID);
			return;
		}
		
		// Initiate the device search.
		searchForDevice();
	}
		
	/***********************************************************************
     * Attempts to perform a clean shutdown of the BtleDevice connection.
     * On success or failure, onDeviceConnectionDropped() is called with
     * status code.
     ***********************************************************************/
	public void closeDeviceConnection() {
		stopDeviceNotifications();
		
		if(mBTGatt != null) {
			mBTGatt.disconnect();
		}
	}
	
	/***********************************************************************
     * Tell the device to start data updates to the client.
     * @param	referenceId
     * 	Reference value to use when transferring data to client using callback.
     ***********************************************************************/
	public void enableSensorDataUpload(int referenceId) {
		setReferenceId(referenceId);
		setNotificationEnabled();
	}
	
	/***********************************************************************
     * Tell the device to stop sending data to the client.
     ***********************************************************************/
	public void disableSensorDataUpload() {
		setReferenceId(DEFAULT_REFERENCE_ID);
		setNotificationDisabled();
	}
	
	/*
     * initBt(), isBtEnabled(), isBleAvailable()
     * 		Functions to get access to the Android Bluetooth service and
     * 		to check if Bluetooth radio is currently on and BT LE feature
     * 		is available on device.
     */
	private boolean initBt() {
		mBTManager = (BluetoothManager) mAppContext.getSystemService(Context.BLUETOOTH_SERVICE);
		if(mBTManager != null) {
			mBTAdapter = mBTManager.getAdapter();
		} else {
			logDebug("(initBt) Unable to access Android Bluetooth Service.");
		}
		
		return (mBTManager != null) && (mBTAdapter != null);
	}
	
	private boolean isBtEnabled() {
		if(!mBTAdapter.isEnabled()) {
			logDebug("(isBtEnabled) Bluetooth is disabled on device.");
			return false;
		}
		return true;
	}
	
	private boolean isBleAvailable() {
		boolean hasBle = mAppContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
		if(hasBle && mBTManager != null && mBTAdapter != null) {
			return true;
		}
		else {
			logDebug("(isBleAvailable) Bluetooth LE is not available on this device.");
			return false;
		}
	}
	
	/*
     * Initiates a search for the device type specified in BtleDeviceType.
     */
	private void searchForDevice() {
		
		UUID[] uuids = new UUID[] { getBtleDeviceType().serviceUUID() };
		
		// Start search, results returned in callback routine
		if(mBTAdapter.startLeScan(uuids, mDeviceFoundCallback)) {
			
		} else {
			logDebug("(searchForDevice) startLeScan call failed");
			mDeviceCallback.onFailedDeviceConnection(getBtleDevice(), BtleDeviceStatus.BTLE_LE_SCAN_START_FAILED);
			return;
		}
		
		logDebug("(searchForDevice) call to startLeScan initiated");
		
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
            	logDebug(String.format("(searchForDevice) mBTAdapter.isDiscovering == %s", mBTAdapter.isDiscovering()?"TRUE":"FALSE"));
				if(mBTAdapter.isDiscovering() == false) return;
				logDebug(String.format("(searchForDevice) Unable to find %s.", getBtleDeviceType().toString()));
				mBTAdapter.stopLeScan(mDeviceFoundCallback);
				mDeviceCallback.onFailedDeviceConnection(getBtleDevice(), BtleDeviceStatus.BTLE_GATT_SERVER_CONNECTION_TIMEOUT);
            }
        }, 10000); // wait 10 seconds		
	}
	
	/*
     * Callback definition required by BluetoothAdapter
     */
	private BluetoothAdapter.LeScanCallback mDeviceFoundCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
        	
        	logDebug(String.format("(onLeScan) Found %s, address: %s", getBtleDeviceType().toString(), device.getAddress()));
        	
        	if(device.getAddress().equals(mBtleDeviceAddress)) {
        		// Save the BluetoothDevice object that was passed
        		mBluetoothDevice = device;
        		mBTAdapter.stopLeScan(mDeviceFoundCallback);
        		connectDevice();
        	}
        	else {
        		logDebug("(onLeScan) Device " + device.getAddress() + " is not target device, continuing search...");
        	}
        }
    };
	
    /*
     * Define a connection with a device by acquiring BluetoothGatt object
     */
    private void connectDevice() {
    	mBtleDeviceName = mBluetoothDevice.getName();
		logDebug("(connectDevice) Connecting to the device NAME: " + mBtleDeviceName + " address: " + mBtleDeviceAddress);
		mBTGatt = mBluetoothDevice.connectGatt(mAppContext, false, mGattCallback);
		logDebug("(connectDevice) mBTGatt returned value, waiting for callback");
		
		Runnable timeout = new Runnable() {
            @Override
            public void run() {
				if(mDeviceConnected) return;
				mDeviceCallback.onFailedDeviceConnection(getBtleDevice(), BtleDeviceStatus.BTLE_GATT_SERVER_CONNECTION_TIMEOUT);
            }
        };
        mHandler.postDelayed(timeout, 10000); // wait 5 seconds		
	}
    
    /*
     * Callback definition required by BluetoothGatt
     */
    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
            	logDebug("(onConnectionStateChange) Device " + mBtleDeviceName + " connected, starting service discovery.");
            	mBTGatt.discoverServices();
            }
            else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            	logDebug("(onConnectionStateChange) Device " + mBtleDeviceName + " disconnected, status " + status);
            	mDeviceConnected = false;
            	mBTGatt.close();
            	mBTGatt = null;
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        	if(status == BluetoothGatt.GATT_SUCCESS) {
        		logDebug("(onServicesDiscovered) Service discovery complete.");
        		analyzeServices();
        	}
        	else {
        		logDebug("(onServicesDiscovered) Unable to discover services");
        		mDeviceCallback.onFailedDeviceConnection(getBtleDevice(),
        				BtleDeviceStatus.BTLE_SERVICE_DISCOVERY_FAILED);
        	}
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt,
                                            BluetoothGattCharacteristic characteristic)
        {
        	if(characteristic.equals(mBTValueCharacteristic)) {
        		sendDeviceDataToClient();
        	}
        }       
        
        @Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic,
                                         int status) {
        	// Do nothing for now
        }


        
        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        	// Do nothing
        };
        
        @Override
        public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
        	// Do nothing
        };
    };
    
    /*
     * Check for available services.
     */
    private void analyzeServices() {
		logDebug("(analyzeServices) Checking services available");
		
		List<BluetoothGattService> services = mBTGatt.getServices();
		
		for(BluetoothGattService s: services) {
			if(s.getUuid().equals(mBtleDeviceType.serviceUUID())) {
				logDebug("(analyzeServices) Found " + mBtleDeviceType.toString() + " service.");
				connectToBtleService();
				return;
			}
		}
		
		logDebug("(analyzeServices) Device "+ mBtleDeviceName +" does not have a valid service that we support.");
		mDeviceCallback.onFailedDeviceConnection(this, BtleDeviceStatus.BTLE_SERVICE_NOT_FOUND);
	}
    
    /*
     * Connect to the service that we found previously.
     */
    private void connectToBtleService() {
    	logDebug(String.format("(connectToBtleService) Connecting to %s service", mBtleDeviceType.toString()));
		mBTService = mBTGatt.getService(mBtleDeviceType.serviceUUID());
		
		if(mBTService == null) {
			logDebug(String.format("(connectToBtleService) Could not connect to %s service", mBtleDeviceType.toString()));
			mDeviceCallback.onFailedDeviceConnection(this, BtleDeviceStatus.BTLE_SERVICE_CONNECTION_FAILED);
		}
		else {
			logDebug(String.format("(connectToBtleService) Successfully connected to %s service.", mBtleDeviceType.toString()));
			getNotifiableCharacteristic();
		}
    }
    
    /*
     * Attempt to acquire the characteristic we want notifications from.
     */
    private void getNotifiableCharacteristic() {
		logDebug(String.format("(getNotifiableCharacteristic) Acquiring %s measurement characteristic", mBtleDeviceType.toString()));
		mBTValueCharacteristic = mBTService.getCharacteristic(mBtleDeviceType.notifiableCharacteristicUUID());
		
		if(mBTValueCharacteristic == null) {
			logDebug(String.format("(getNotifiableCharacteristic) Unable to find %s measurement characteristic", mBtleDeviceType.toString()));
			mDeviceCallback.onFailedDeviceConnection(this, BtleDeviceStatus.BTLE_CHARACTERISTIC_NOT_FOUND);
		}
		else {
			logDebug(String.format("(getNotifiableCharacteristic) %s Measurement characteristic retrieved.", mBtleDeviceType.toString()));
			startDeviceNotifications();
		}
	}
    
    /*
     * Start device notifications.
     */
    private void startDeviceNotifications() {
		logDebug(String.format("Enabling notification for %s.", mBtleDeviceName));
        boolean success = mBTGatt.setCharacteristicNotification(mBTValueCharacteristic, true);
        if(!success) {
        	logDebug(String.format("(startDeviceNotifications) Failed to enable notification for %s", mBtleDeviceName));
        	mDeviceCallback.onFailedDeviceConnection(this, BtleDeviceStatus.BTLE_CHARACTERISTIC_SET_FAILED);
        	return;
        }

        BluetoothGattDescriptor descriptor = mBTValueCharacteristic.getDescriptor(BtleDeviceType.getCharClientConfigDescriptor());
        if(descriptor != null) {
	        descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
	        mBTGatt.writeDescriptor(descriptor);
	        logDebug(String.format("(startDeviceNotifications) Enabled notification for %s", mBtleDeviceName));
	        
	        // Inform the client that this device is ready.
	        mDeviceConnected = true;
	        mDeviceCallback.onSuccessfulDeviceConnection(this);
        }		
        else {
        	logDebug(String.format("(startDeviceNotifications) Failed to acquire characteristic descriptor for %s, notifications are not enabled.", mBtleDeviceName));
        	mDeviceCallback.onFailedDeviceConnection(this, BtleDeviceStatus.BTLE_CHARACTERISTIC_NOTIFY_START_FAILED);
        }
	}
    
    /*
     * Stop device notifications. Only call this when we're in the 
     * process of disconnecting from the BtleDevice.
     */
    private boolean stopDeviceNotifications() {
    	if(mBTGatt == null || mBTValueCharacteristic == null) {
			return true;
		}
		
		logDebug("(stopDeviceNotifications) Disabling notification for " + mBtleDeviceName);

        boolean success = mBTGatt.setCharacteristicNotification(mBTValueCharacteristic, false);
        if(!success) {
        	logDebug("(stopDeviceNotifications) Disabling notification failed! Device: " + mBtleDeviceName);
        	return false;
        }

        BluetoothGattDescriptor descriptor = mBTValueCharacteristic.getDescriptor(BtleDeviceType.getCharClientConfigDescriptor());
        if(descriptor != null) {
	        descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
	        if(mBTGatt != null) {
	        	mBTGatt.writeDescriptor(descriptor);
	        }
	        logDebug("(stopDeviceNotifications) Notification disabled for " + mBtleDeviceName);
	        return true;
        }		
        else {
        	logDebug("(stopDeviceNotifications) Could not get descriptor for characteristic! " +
        			"Notification could be still enabled on " + mBtleDeviceName);
        	return false;
        }
    }
    
    /*
     * Determine what type of sensor we're dealing with right now and
     * pass control to the appropriate handler.
     */
    private void sendDeviceDataToClient() { 	
    	switch(mBtleDeviceType) {
    	case HEART_RATE_MONITOR:
    		getHeartRateData();
    		break;
		case CYCLING_POWER_METER:
			getCyclePowerMeasurementData();
			break;
		case CYCLING_SPEED_CADENCE_SENSOR:
			getCyclingSpeedAndCadenceMeasurementData();
			break;
		default:
			break;
    	}
	}
    
    /*
     * Parse the data from the Heart Rate Monitor and pass up to the client.
     */
    private void getHeartRateData() {
		byte[] raw = mBTValueCharacteristic.getValue();
    	int index = ((raw[0] & 0x01) == 1) ? 2 : 1;
    	int format = (index == 1) ? BluetoothGattCharacteristic.FORMAT_UINT8 : BluetoothGattCharacteristic.FORMAT_UINT16;
    	int value = mBTValueCharacteristic.getIntValue(format, index);
    	long timestamp = Calendar.getInstance().getTimeInMillis();
    	final String description = "(" + timestamp + ")" + " " + value + " bpm " + ",refId: " + mReferenceId;
    	
    	logDebug("(getHeartRateData) " + description);
    	
    	if(isSensorDataUploadEnabled()) {
    		mDeviceCallback.transferCharacteristicData_heartRate(this, timestamp, getReferenceId(), value);
    	}
	}
	
    /*
     * Parse the data from the Cycling Power Meter and pass up to the client.
     */
	private void getCyclePowerMeasurementData() {
		// TODO
		// Fix so that we are properly handling all characteristic data
		// put out by power meter class of devices.
		final int instantPowerWatts = mBTValueCharacteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 2);
		long timestamp = Calendar.getInstance().getTimeInMillis();
		final String description = "(" + timestamp + ")" + " " + instantPowerWatts + " watts";
		
		logDebug("(getCyclePowerMeasurementData) " + description);
		if(isSensorDataUploadEnabled()) {
			mDeviceCallback.transferCharacteristicData_cyclePower(this,
					timestamp, getReferenceId(), instantPowerWatts,
					0.0, 0, 0, -1, 0, 0, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0);
			
			/*mDeviceCallback.transferCharacteristicData_cyclePower(
					timestamp, getReferenceId(), instantPowerWatts, pedalPowerBalance, 
					accumulatedTorque, cumWheelRevs, cumCrankRevs, lastWheelTime, lastCrankTime,
					maxForce, minForce, maxTorque, minTorque, maxAngle, minAngle, topDeadSpotAngle, 
					bottomDeadSpotAngle, accumulatedEnergy);*/
		}
	}
	
	
	/*
     * Parse the data from the Cycling Speed and Cadence sensor and pass
     * up to the client.
     */
	private void getCyclingSpeedAndCadenceMeasurementData() {
		long timestamp = Calendar.getInstance().getTimeInMillis();
		final int totalWheelRevs = mBTValueCharacteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 1);
		final int totalCrankRevs = mBTValueCharacteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 7);
		final int lastWheelTime = mBTValueCharacteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 5);
		final int lastCrankTime = mBTValueCharacteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 9);
		final String description = "(" + timestamp + ")" + " " + totalWheelRevs + " wheel revs, " + totalCrankRevs + " crank revs";
		
		logDebug("(getCyclingSpeedAndCadenceMeasurementData) " + description);
		if(isSensorDataUploadEnabled()) {
	    	mDeviceCallback.transferCharacteristicData_cycleSpeedCadence(this,
	    			timestamp, getReferenceId(), totalWheelRevs, totalCrankRevs, lastWheelTime, lastCrankTime);
		}
	}
}
